{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d895cdd8-f01e-4280-9f93-46456f9a0f92",
   "metadata": {},
   "source": [
    "# Large Vector Fields Dynamically Downsampled\n",
    "\n",
    "The visualization of vector fields from large datasets often presents a challenge. Direct plotting methods can quickly consume excessive memory, leading to crashes or unresponsive applications. To address this issue, we introduce a dynamic downsampling technique that enables interactive exploration of vector fields without sacrificing performance. We first create a `sample_data` that contains 4,000,000 points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86995d31-9905-402e-985d-8fa5760aac73",
   "metadata": {},
   "outputs": [],
   "source": [
    "import holoviews as hv\n",
    "import hvplot.xarray  # noqa\n",
    "import numpy as np\n",
    "import xarray as xr"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1378e546-86ea-4153-8cca-7776b2a2389c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sample_data(shape=(20, 30)):\n",
    "    x = np.linspace(311.9, 391.1, shape[1])\n",
    "    y = np.linspace(-23.6, 24.8, shape[0])\n",
    "    x2d, y2d = np.meshgrid(x, y)\n",
    "    u = 10 * (2 * np.cos(2 * np.deg2rad(x2d) + 3 * np.deg2rad(y2d + 30)) ** 2)\n",
    "    v = 20 * np.cos(6 * np.deg2rad(x2d))\n",
    "    return x, y, u, v\n",
    "\n",
    "xs, ys, U, V = sample_data(shape=(2000, 2000))\n",
    "mag = np.sqrt(U**2 + V**2)\n",
    "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n",
    "ds = xr.Dataset({\n",
    "    'mag': xr.DataArray(mag, dims=('y', 'x'), coords={'y': ys, 'x': xs}),\n",
    "    'angle': xr.DataArray(angle, dims=('y', 'x'), coords={'y': ys, 'x': xs})\n",
    "})\n",
    "ds"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a56159d4-b26b-4ec8-b088-d3b3d4d4e27a",
   "metadata": {},
   "source": [
    "If we just try to call `ds.hvplot.vectorfield` this is likely raising a `MemoryError`. The alternative is to dynamically downsample the data based on the visible range. This helps manage memory consumption when dealing with large datasets, especially when plotting vector fields. We are going to use HoloViews to create a view ([`hv.DynamicMap`](https://holoviews.org/reference/containers/bokeh/DynamicMap.html)) whose content is dynamically updated based on the data range displayed (tracked by the [`hv.streams.RangeXY`](https://holoviews.org/reference/streams/bokeh/RangeXY.html) stream), find out more about these concepts in [HoloViews' documentation](https://holoviews.org/user_guide/Custom_Interactivity.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de730785-a725-4052-a60a-78e05cf6cd96",
   "metadata": {},
   "outputs": [],
   "source": [
    "def downsample_quiver(x_range=None, y_range=None, nmax=10):\n",
    "    \"\"\"\n",
    "    Creates a HoloViews vector field plot from a dataset, dynamically downsampling \n",
    "    data based on the visible range to optimize memory usage.\n",
    "\n",
    "    Args:\n",
    "        x_range (tuple, optional): Range of x values to include. Defaults to None (full range).\n",
    "        y_range (tuple, optional): Range of y values to include. Defaults to None (full range).\n",
    "        nmax (int, optional): Maximum number of points along each axis after coarsening. \n",
    "                              Defaults to 10.\n",
    "\n",
    "    Returns:\n",
    "        HoloViews DynamicMap: A dynamic vector field plot that updates based on the visible range.\n",
    "    \"\"\"\n",
    "    if x_range is None or y_range is None:\n",
    "        # No range provided, downsample the entire dataset for initial display\n",
    "        xs, ys = ds.x.size, ds.y.size        # Get dataset dimensions\n",
    "    else:\n",
    "        # Select data within the specified range\n",
    "        sub = ds.sel(x=slice(*x_range), y=slice(*y_range))\n",
    "        # Downsample the selected data\n",
    "        xs, ys = sub.x.size, sub.y.size\n",
    "\n",
    "    ix, iy = xs // nmax, ys // nmax     # Calculate downsampling intervals\n",
    "    ix = max(1, ix)                     # Ensure interval is at least 1\n",
    "    iy = max(1, iy)\n",
    "    sub = ds.coarsen(x=ix, y=iy, side=\"center\", boundary=\"trim\").mean()  # Downsample\n",
    "\n",
    "    # Create the vector field plot\n",
    "    quiver = sub.hvplot.vectorfield(\n",
    "        x=\"x\", y=\"y\", mag=\"mag\",\n",
    "        angle=\"angle\", hover=False,\n",
    "    ).opts(magnitude=\"mag\")\n",
    "    return quiver"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "edffb1cb-8d68-49f9-9f38-2a2ce9521ff2",
   "metadata": {},
   "source": [
    "Zoom in and out to observe the vector field being dynamically downsampled based on the current view.\n",
    "\n",
    ":::{note}\n",
    "Run this example locally to see these effects take place.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ecd32c5b-22a6-4ff8-9449-2b387a800f03",
   "metadata": {},
   "outputs": [],
   "source": [
    "range_xy = hv.streams.RangeXY()  # Stream to capture range changes\n",
    "filtered = hv.DynamicMap(downsample_quiver, streams=[range_xy])  # Dynamic plot\n",
    "filtered"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d70b155-e707-44c7-80c3-0fc5eee41b78",
   "metadata": {},
   "source": [
    ":::{seealso}\n",
    "- [Vector Field reference documentation](../../ref/api/manual/hvplot.hvPlot.vectorfield.ipynb).\n",
    "- [Resampling Options](../../ref/plotting_options/resampling.ipynb).\n",
    "- [HoloViews Custom Interactivity Guide](https://holoviews.org/user_guide/Custom_Interactivity.html).\n",
    ":::"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
